﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections;
using System.ComponentModel;
using System.Windows.Media.Imaging;
using System.Windows.Media;
using System.Windows;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace MTS
{
    public class Main
    {
        private String saveDir;
        BackgroundWorker worker = new BackgroundWorker();

        public Main()
        {
            saveDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ".minecraft\\saves");
        }

        public BitmapSource renderWorldOblique(String worldPath, BackgroundWorker worker)
        {
            // Render the world
            DateTime startTime = DateTime.Now;
            List<String> chunkFiles = this.getFolder(worldPath);
            List<TileImage> tiles = new List<TileImage>();
            List<LevelChunk> chunks = null;

            int counter = 0;
            int success = 0;
            int failure = 0;
            int minWorldx = int.MaxValue;
            int maxWorldx = int.MinValue;
            int minWorldy = int.MaxValue;
            int maxWorldy = int.MinValue;
            PixelFormat outputFormat = PixelFormats.Bgra32;
            int _bytesPerPixel = (outputFormat.BitsPerPixel + 7) / 8;
            int maximumChunks = chunkFiles.Count;

            char[] invalidChars = System.IO.Path.GetInvalidFileNameChars();
            String invalidString = Regex.Escape(new String(invalidChars));
            String serialiseName = Regex.Replace(worldPath, "[" + invalidString + "]", "");
            FileInfo serialisedFileInfo = new FileInfo(Path.Combine("ChunkCache", serialiseName));
            IFormatter formatter = new BinaryFormatter();

            if (serialisedFileInfo.Exists)
            {
                using (Stream stream = serialisedFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    chunks = (List<LevelChunk>)formatter.Deserialize(stream);
                }
            }
            else
            {
                chunks = new List<LevelChunk>();
            }
            int newChunks = 0;
            int deadChunks = 0;
            int existingChunks = 0;
            foreach (LevelChunk current in chunks)
            {
                if (chunkFiles.Contains(current.fileInfo.FullName))
                {
                    // Remove it off the string list
                    chunkFiles.Remove(current.fileInfo.FullName);
                    existingChunks++;
                } 
                else
                {
                    // Remove it from the level chunk list
                    Console.Out.WriteLine("Removed chunk {0}", current.fileInfo.FullName);
                    chunks.Remove(current);
                    deadChunks++;
                }
            }
            // All that will be left on the string list is the new chunks
            foreach (String chunkPath in chunkFiles)
            {
                //Create new LevelChunks
                LevelChunk newChunk = new LevelChunk(new FileInfo(chunkPath), _bytesPerPixel);
                chunks.Add(newChunk);
                newChunks++;
            }
            Console.Out.WriteLine("{0} chunks added, {1} chunks removed, {2} chunks from cache", newChunks, deadChunks, existingChunks);
            Parallel.ForEach(chunks, current =>
            {
                if (current.hasBlocks)
                {
                    minWorldx = Math.Min(minWorldx, (int)current.worldX);
                    maxWorldx = Math.Max(maxWorldx, (int)current.worldX);
                    minWorldy = Math.Min(minWorldy, (int)current.worldY);
                    maxWorldy = Math.Max(maxWorldy, (int)current.worldY);
                    TileImage obliqueTile = new TileImage(current.obliqueImage, current.worldX, current.worldY, 16, 16 + 129, TileImage.TileType.OBLIQUE);
                    lock (tiles)
                    {
                        tiles.Add(obliqueTile);
                    }

                    success++;
                    worker.ReportProgress((int)((float)success / (float)maximumChunks*100));
                }
                else
                {
                    failure++;
                    maximumChunks--;
                    Console.Out.WriteLine("Could not read chunk {0}", current.getFileName());
                }
            });
            if (!serialisedFileInfo.Directory.Exists)
            {
                serialisedFileInfo.Directory.Create();
            }
            using (Stream stream = serialisedFileInfo.Open(FileMode.Create, FileAccess.Write, FileShare.None))
            {
                formatter.Serialize(stream, chunks );
            }
            counter++;
            Console.Out.WriteLine("Moving on to processing tiles");
            long worldWidth = maxWorldx - minWorldx;
            long worldHeight = maxWorldy - minWorldy;
            int worldImageWidth = ((int)worldWidth + 1) * 16;
            int worldImageHeight = ((int)worldHeight + 1) * 16 + 129;

            WriteableBitmap outputBitmap = new WriteableBitmap(worldImageWidth, worldImageHeight, 96, 96, outputFormat, null);
            tiles.Sort();
            Console.Out.WriteLine("World is {0}x{1}", worldWidth, worldHeight);
            Console.Out.WriteLine("Min: {0}x{1}", minWorldx, minWorldy);
            Console.Out.WriteLine("Max: {0}x{1}", maxWorldx, maxWorldy);
            int currentIndex = 0;

            foreach (TileImage current in tiles)
            {
                if (current.getTileType().Equals(TileImage.TileType.OBLIQUE))
                {
                    byte[] inputImageBytes = current.getImage();
                    int _innerStride = current.getWidth() * _bytesPerPixel;
                    Int32 destinationX = ((int)current.getX() - minWorldx) * 16;
                    Int32 destinationY = ((int)current.getY() - minWorldy) * 16;
                    Int32Rect destinationRect = new Int32Rect(destinationX, destinationY, 16, 145);
                    outputBitmap.Lock();
                    unsafe
                    {
                        int pBackBuffer = (int)outputBitmap.BackBuffer;
                        // Go to the first pixel from the global coordinates
                        pBackBuffer += destinationRect.Y * outputBitmap.BackBufferStride;
                        pBackBuffer += destinationRect.X * _bytesPerPixel;
                        // Now iterate over each pixel
                        for (int y = 0; y < destinationRect.Height; y++)
                        {
                            for (int x = 0; x < destinationRect.Width; x++)
                            {
                                // Insert the pixel then move the backbuffer on a pixel
                                int byteOffset = (x + y * current.getWidth()) * _bytesPerPixel;

                                int existing_color_data = *((int*)pBackBuffer);
                                Color existing_color = Color.FromArgb((byte)((existing_color_data & 0xFF000000) >> 24),
                                                                        (byte)((existing_color_data & 0x00FF0000) >> 16),
                                                                        (byte)((existing_color_data & 0x0000FF00) >> 8),
                                                                        (byte)((existing_color_data & 0x000000FF) >> 0));

                                Color input_color = Color.FromArgb(inputImageBytes[byteOffset + 3], 
                                                                    inputImageBytes[byteOffset + 2], 
                                                                    inputImageBytes[byteOffset + 1], 
                                                                    inputImageBytes[byteOffset]);
                                Color outputColor = this.blendColors(input_color, existing_color, 128);

                                int color_data = outputColor.A << 24; // A
                                color_data |= outputColor.R << 16; // R
                                color_data |= outputColor.G << 8; // G
                                color_data |= outputColor.B << 0; // B

                                *((int*)pBackBuffer) = color_data;
                                pBackBuffer += _bytesPerPixel;
                            }
                            // Move on to the start of the next row (backbufferstride - width * bytes per pixel)
                            pBackBuffer += (outputBitmap.BackBufferStride - destinationRect.Width * _bytesPerPixel);
                        }
                        outputBitmap.AddDirtyRect(destinationRect);
                    }
                    outputBitmap.Unlock();
                    worker.ReportProgress((int)((float)currentIndex / (float)maximumChunks * 100));
                    currentIndex++;
                }
            }

            outputBitmap.Freeze();

            TimeSpan elapsed = DateTime.Now - startTime;
            Console.Out.WriteLine("Read {0} chunks, {1} successes and {2} failures in {3}", chunkFiles.Count, success, failure, elapsed);
            return outputBitmap;
        }

        public BitmapSource renderWorldTerrain(String worldPath, BackgroundWorker worker)
        {
            // Render the world
            DateTime startTime = DateTime.Now;
            List<String> chunkFiles = getFolder(worldPath);
            List<TileImage> tiles = new List<TileImage>();
            List<LevelChunk> chunks = null;

            int counter = 0;
            int success = 0;
            int failure = 0;
            int minWorldx = int.MaxValue;
            int maxWorldx = int.MinValue;
            int minWorldy = int.MaxValue;
            int maxWorldy = int.MinValue;
            PixelFormat outputFormat = PixelFormats.Bgra32;
            int _bytesPerPixel = (outputFormat.BitsPerPixel + 7) / 8;
            int maximumChunks = chunkFiles.Count;
            char[] invalidChars = System.IO.Path.GetInvalidFileNameChars();
            String invalidString = Regex.Escape(new String(invalidChars));
            String serialiseName = Regex.Replace(worldPath, "[" + invalidString + "]", "");
            FileInfo serialisedFileInfo = new FileInfo(Path.Combine("ChunkCache", serialiseName));
            IFormatter formatter = new BinaryFormatter();

            if (serialisedFileInfo.Exists)
            {
                using (Stream stream = serialisedFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    chunks = (List<LevelChunk>)formatter.Deserialize(stream);
                }
            }
            else
            {
                chunks = new List<LevelChunk>();
            }
            int newChunks = 0;
            int deadChunks = 0;
            int existingChunks = 0;
            foreach (LevelChunk current in chunks)
            {
                if (chunkFiles.Contains(current.fileInfo.FullName))
                {
                    // Remove it off the string list
                    chunkFiles.Remove(current.fileInfo.FullName);
                    existingChunks++;
                }
                else
                {
                    // Remove it from the level chunk list
                    Console.Out.WriteLine("Removed chunk {0}", current.fileInfo.FullName);
                    chunks.Remove(current);
                    deadChunks++;
                }
            }
            // All that will be left on the string list is the new chunks
            foreach (String chunkPath in chunkFiles)
            {
                //Create new LevelChunks
                LevelChunk newChunk = new LevelChunk(new FileInfo(chunkPath), _bytesPerPixel);
                chunks.Add(newChunk);
                newChunks++;
            }
            Console.Out.WriteLine("{0} chunks added, {1} chunks removed, {2} chunks from cache", newChunks, deadChunks, existingChunks);
            Parallel.ForEach(chunks, current =>
            {
                if (current.hasBlocks)
                {
                    minWorldx = Math.Min(minWorldx, (int)current.worldX);
                    maxWorldx = Math.Max(maxWorldx, (int)current.worldX);
                    minWorldy = Math.Min(minWorldy, (int)current.worldY);
                    maxWorldy = Math.Max(maxWorldy, (int)current.worldY);
                    TileImage currentTile = new TileImage(current.terrainImage, current.worldX, current.worldY, 16, 16, TileImage.TileType.TERRAIN);
                    lock (tiles)
                    {
                        tiles.Add(currentTile);
                    }
                    success++;
                    worker.ReportProgress((int)((float)success / (float)maximumChunks * 100));
                }
                else
                {
                    failure++;
                    Console.Out.WriteLine("Could not read chunk {0}", current.getFileName());
                }
                counter++;
            });
            if (!serialisedFileInfo.Directory.Exists)
            {
                serialisedFileInfo.Directory.Create();
            }
            using (Stream stream = serialisedFileInfo.Open(FileMode.Create, FileAccess.Write, FileShare.None))
            {
                formatter.Serialize(stream, chunks);
            }
            Console.Out.WriteLine("Moving on to processing tiles");
            long worldWidth = maxWorldx - minWorldx;
            long worldHeight = maxWorldy - minWorldy;
            tiles.Sort();
            WriteableBitmap outputImage = new WriteableBitmap(((int)worldWidth + 1) * 16, ((int)worldHeight + 1) * 16, 96, 96, outputFormat, null);

            int _stride = outputImage.PixelWidth * _bytesPerPixel;

            Console.Out.WriteLine("World is {0}x{1}", worldWidth, worldHeight);
            Console.Out.WriteLine("Min: {0}x{1}", minWorldx, minWorldy);
            Console.Out.WriteLine("Max: {0}x{1}", maxWorldx, maxWorldy);
            int currentIndex = 0;
            foreach (TileImage current in tiles)
            {
                if (current.getTileType().Equals(TileImage.TileType.TERRAIN))
                {
                    byte[] inputImage = current.getImage();
                    int _innerStride = current.getWidth() * _bytesPerPixel;
                    Int32 destinationX = ((int)current.getX() - minWorldx) * 16;
                    Int32 destinationY = ((int)current.getY() - minWorldy) * 16;
                    Int32Rect sourceRect = new Int32Rect(0, 0, current.getWidth(), current.getHeight());
                    outputImage.WritePixels(sourceRect, inputImage, _innerStride, destinationX, destinationY);
                    worker.ReportProgress((int)((float)currentIndex / (float)maximumChunks * 100));
                    currentIndex++;
                }
            }
            outputImage.Freeze();

            TimeSpan elapsed = DateTime.Now - startTime;
            Console.Out.WriteLine("Read {0} chunks, {1} successes and {2} failures in {3}", chunkFiles.Count, success, failure, elapsed);

            return outputImage;

        }

        private List<String> getFolder(String folder)
        {
            List<String> files = new List<String>();
            foreach (String directory in Directory.GetDirectories(folder))
            {
                files.AddRange(getFolder(directory));
            }
            foreach (String fileItem in Directory.GetFiles(folder, "*.dat"))
            {
                files.Add(fileItem);
            }
            return files;
        }

        private Color blendColors(Color A, Color B, int h)
        {
            Color C = new Color();
            float Aa = A.ScA;
            float Ba = B.ScA;
            float Alpha = Aa + Ba * (1 - Aa);
            C.R = (byte)((float)A.R * Aa + (float)B.R * Ba * (1 - Aa) * (float)h / 128 / Alpha);
            C.G = (byte)(float)(A.G * Aa + (float)B.G * Ba * (1 - Aa) * (float)h / 128 / Alpha);
            C.B = (byte)((float)A.B * Aa + (float)B.B * Ba * (1 - Aa) * (float)h / 128 / Alpha);
            C.A = (byte)(Alpha * 255);
            return C;
        }
    }
}
